/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package fr.loof.fonzie.apt;

import static javax.lang.model.SourceVersion.RELEASE_5;
import static javax.lang.model.element.ElementKind.CLASS;
import static javax.tools.Diagnostic.Kind.ERROR;
import static javax.tools.Diagnostic.Kind.NOTE;
import static javax.tools.Diagnostic.Kind.WARNING;

import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;

import com.sun.source.util.Trees;
import com.sun.tools.javac.code.Flags;
import com.sun.tools.javac.processing.JavacProcessingEnvironment;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.TreeMaker;
import com.sun.tools.javac.tree.TreeTranslator;
import com.sun.tools.javac.tree.JCTree.JCBlock;
import com.sun.tools.javac.tree.JCTree.JCExpression;
import com.sun.tools.javac.tree.JCTree.JCMethodDecl;
import com.sun.tools.javac.tree.JCTree.JCMethodInvocation;
import com.sun.tools.javac.tree.JCTree.JCStatement;
import com.sun.tools.javac.tree.JCTree.JCVariableDecl;
import com.sun.tools.javac.util.Context;
import com.sun.tools.javac.util.List;
import com.sun.tools.javac.util.Name;
import com.sun.tools.javac.util.Name.Table;

/**
 * @see http://scg.unibe.ch/archive/projects/Erni08b.pdf
 * @see http://www.docjar.com/html/api/com/sun/tools/javac/tree/TreeMaker.java.html
 * @author ndeloof
 */
@SupportedAnnotationTypes( "javax.persistence.Entity" )
@SupportedSourceVersion( RELEASE_5 )
public class ORMJavacAnnotationProcessor
    extends AbstractProcessor
{
    private class FonzieCodeIntroducer
        extends TreeTranslator
    {
        @Override
        public void visitMethodDef( JCMethodDecl method )
        {
            if ( isNative( method ) )
            {
                method.mods.flags -= Flags.NATIVE;
                method.restype = translate( method.restype );
                method.typarams = translateTypeParams( method.typarams );
                method.params = translateVarDefs( method.params );
                method.thrown = translate( method.thrown );
                method.body = introduceFonzieCode( method );
            }
            else
            {
                super.visitMethodDef( method );
            }
            result = method;
        }

        private JCBlock introduceFonzieCode( JCMethodDecl method )
        {
            if ( method.getName().toString().equals( "persist" ) )
            {
                return block(
                    parse( "fr.loof.fonzie.Fonzie fonzie = fr.loof.fonzie.Fonzie.instance()" ),
                    parse( "fonzie.persist( this )" )
                );
            }
            else
            {
                throw new UnsupportedOperationException( "Not supported yet" );
            }
        }
    }

    private Trees trees;

    private TreeMaker maker;

    private Name.Table names;

    @Override
    public synchronized void init( ProcessingEnvironment env )
    {
        super.init( env );
        trees = Trees.instance( env );
        // Rabit hole to wonderland :)
        JavacProcessingEnvironment javac = (JavacProcessingEnvironment) env;
        Context context = javac.getContext();
        maker = TreeMaker.instance( context );
        names = Table.instance( context );
    }

    @Override
    public boolean process( Set<? extends TypeElement> annotations, RoundEnvironment roundEnv )
    {
        if ( !roundEnv.processingOver() )
        {
            for ( Element element : roundEnv.getRootElements() )
            {
                if ( element.getKind() == CLASS )
                {
                    // Cast to compiler internal tree node
                    // NB: this is internal undocumented API that may change with Java7
                    JCTree tree = (JCTree) trees.getTree( element );
                    TreeTranslator visitor = new FonzieCodeIntroducer();
                    tree.accept( visitor );
                }
            }
        }
        else
        {
            note( "Entities intrumented by Fonzie" );
        }
        return false;
    }

    // --- Helper methods for Code introduction ---

    private final List<JCExpression> nil = List.<JCExpression> nil();

    public static void main( String[] args )
    {
        new ORMJavacAnnotationProcessor().parse( "fr.loof.fonzie.Fonzie fonzie = fr.loof.fonzie.Fonzie.instance()" );
    }

    private JCStatement parse( String code )
    {
        int k = code.indexOf( " = " );
        if ( k > 0 )
        {
            String methodCall = code.substring( k + 3 );
            int i = code.indexOf( ' ' );
            String type = code.substring( 0, i );
            int j = code.indexOf( ' ', i + 1 );
            String varName = code.substring( i + 1, j );

            JCExpression oType = ident( type );
            JCMethodInvocation oValue = apply( methodCall );
            // FIXME how to get oType from JCMethodInvocation ? oValue.meth.type is null :'(

            JCVariableDecl var =
                maker.VarDef(
                maker.Modifiers( Flags.FINAL ),
                names.fromString( varName ),
                oType, oValue );
            return var;
        }
        else
        {
            return maker.Exec( apply( code ) );
        }
    }

    private JCMethodInvocation apply( String methodCall )
    {
        int l = methodCall.indexOf( '(' );
        String fcn = methodCall.substring( 0, l );
        String argString = methodCall.substring( l + 1, methodCall.length() - 1 );
        List<JCExpression> params = nil;
        if ( argString.length() > 0 )
        {
            String[] args = argString.split( "," );
            JCExpression[] expressions = new JCExpression[args.length];
            int idx = 0;
            for ( String arg : args )
            {
                expressions[idx++] = ident( arg.trim() );
            }
            params = List.<JCExpression> from( expressions );
        }

        return apply( fcn, params );
    }


    private JCMethodInvocation apply( String fcn, List<JCExpression> args )
    {
        JCExpression oMethod = ident( fcn );
        JCMethodInvocation oValue = maker.Apply( nil, oMethod, args );
        return oValue;
    }

    /**
     * Based on Lombok JavacHandlerUtil
     * <p>
     * In javac, dotted access of any kind, from {@code java.lang.String} to {@code var.methodName} is represented
     * by a fold-left of {@code Select} nodes with the leftmost string represented by a {@code Ident} node. This
     * method generates such an expression.
     * <p>
     * For example, maker.Select(maker.Select(maker.Ident(NAME[java]), NAME[lang]), NAME[String]).
     *
     * @see http://projectlombok.org/api/lombok/javac/handlers/JavacHandlerUtil.html
     */
    private JCExpression ident( String fcn )
    {
        String[] parts = fcn.split( "\\." );
        JCExpression e = maker.Ident( names.fromString( parts[0] ) );
        for ( int i = 1; i < parts.length; i++ )
        {
            e = maker.Select( e, names.fromString( parts[i] ) );
        }
        return e;
    }

    private JCBlock block( JCStatement... statements )
    {
        return maker.Block( Flags.BLOCK, List.<JCStatement> from( statements ) );
    }

    private boolean isNative( JCMethodDecl method )
    {
        return (method.mods.flags & Flags.NATIVE) > 0;
    }

    private void note( String msg )
    {
        processingEnv.getMessager().printMessage( NOTE, msg );
    }

    private void warn( String msg )
    {
        processingEnv.getMessager().printMessage( WARNING, msg );
    }

    private void error( String msg )
    {
        processingEnv.getMessager().printMessage( ERROR, msg );
    }
}
